/*  ----------------------------------------------------------------------------
 *  Arello Mobile
 *  Fast 3d Engine
 *  ----------------------------------------------------------------------------
 * Except where otherwise noted, this work is licensed under
 * http://creativecommons.org/licenses/by/3.0
 */

#include "HardwareGL.h"
#include "Light3D.h"
#include "Shader3D.h"
#include "tk.h"
#include "Log.h"
#include <algorithm>
#include <utility>
#include "stdmin.h"


class MatrixGL
{
	Matrix3D mat;
	static void low_save( const GLfloat * f )
	{
		glLoadMatrixf( f );
	}
public:
	MatrixGL( const Matrix3D & mat )
	:	mat( mat )
	{}
	void save()const
	{
		GLfloat data[16];
		for( int y = 0; y < 4; ++y )
			for( int x = 0; x < 4; ++x )
				data[ x*4 + y ] = mat.x( x, y );
//		glLoadIdentity();
		low_save(data);
	}
};

HardwareGL::HardwareGL( HWND main_wnd )
:	main_wnd( main_wnd ),
	light_count( 0 ),
	view_mat( Matrix3D::identity() ),
	world_mat( Matrix3D::identity() )
{
	mode_info["OpenGL|Windowed 320x240|Desktop"] = ModeInfo( 320, 240, 0, false );
	mode_info["OpenGL|Windowed 600x450|Desktop"] = ModeInfo( 600, 450, 0, false );
	mode_info["OpenGL|Windowed 760x570|Desktop"] = ModeInfo( 760, 570, 0, false );
//	mode_info["OpenGL|Fullscreen 320x240|Truecolor"] = ModeInfo( 320, 240, 24, true );
//	mode_info["OpenGL|Fullscreen 640x480|Truecolor"] = ModeInfo( 640, 480, 24, true );
//	mode_info["OpenGL|Fullscreen 640x480|Hicolor"] = ModeInfo( 640, 480, 16, true );
}

std::vector<string> HardwareGL::all_modes()const
{
	std::vector<string> result;

	std::map<string,ModeInfo>::const_iterator mit = mode_info.begin();
	for( ; mit != mode_info.end(); ++mit )
		result.push_back( mit->first );
	return result;
}

string HardwareGL::name()const
{
	return "OpenGL";
}

bool HardwareGL::set_mode( string mode )
{
	if( !curr_mode_name.empty() )
		return false; // Can not set mode twice. Silly hardware...
	std::map<string,ModeInfo>::const_iterator mit = mode_info.find( mode );
	if( mit == mode_info.end() )
		mit = mode_info.begin();

//	if( mit-

	curr_mode_name = mit->first;
	if( mit->second.is_fullscreen )
	{
/*		DEVMODE mode;
		memset(&mode,0,sizeof(mode));
		mode.dmSize=sizeof(mode);
		mode.dmBitsPerPel=mit->second.bits;
		mode.dmPelsWidth=scr_x;
		mode.dmPelsHeight=scr_y;
		mode.dmDisplayFlags=0;
		mode.dmDisplayFrequency=60;
		mode.dmFields=DM_BITSPERPEL |
			DM_PELSWIDTH |
			DM_PELSHEIGHT;
		Log("Changing Video to %dx%d (%d bits)\n",mode.dmPelsWidth,mode.dmPelsHeight,mode.dmBitsPerPel);
		switch( ChangeDisplaySettings( &mode, 0 ) )
		{
		case DISP_CHANGE_SUCCESSFUL:
			Log("The settings change was successful. \n");
			break; 
		case DISP_CHANGE_RESTART:
			Log("The computer must be restarted in order for the graphics mode to work. \n");
			break;
		case DISP_CHANGE_BADFLAGS:
			Log("An invalid set of flags was passed in. \n");
			break;
		case DISP_CHANGE_FAILED:
			Log("The display driver failed the specified graphics mode. \n");
			break;
		case DISP_CHANGE_BADMODE:
			Log("The graphics mode is not supported. \n");
			break;
		}*/
	}
	MoveWindow( main_wnd,0,0,mit->second.scr_x,mit->second.scr_y, true );

	tkInitDisplayMode( TK_DOUBLE | TK_RGB | TK_DEPTH );
//	tkInitDisplayMode( TK_SINGLE | TK_RGB | TK_DEPTH );

	tkInitPosition(0, 0, \
		GetSystemMetrics(SM_CXSCREEN), \
		GetSystemMetrics(SM_CYSCREEN),1 );

	tkCreateContext( main_wnd );
	return true;
}

void HardwareGL::stop_mode()
{
	textures.clear();
	failed_textures.clear();

	curr_mode_name = "";
	tkDestroyWindow();

//	ChangeDisplaySettings( NULL, 0 );
}

HardwareGL::~HardwareGL()
{
	stop_mode();
}

void HardwareGL::start_viewport( float left, float top, 
						   float right, float bottom, Color back )
{
	const float z_buf_range = 128.0f;
//		glClearDepth(1.0);
//	glClearColor(0,0.2,0,0);
//		glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
	RECT rect;
	GetClientRect( main_wnd, &rect );
//	glViewport( rect.left, rect.top, rect.right, rect.bottom );
	// Set ViewPort
	int l = rect.left + (rect.right - rect.left) * left;
	int dx = (rect.right - rect.left) * (right - left);
	int t = rect.top + (rect.bottom - rect.top) * top;
	int dy = (rect.bottom - rect.top) * (bottom - top);
	glViewport( l, t, dx, dy );

	if( left == 0 && right == 1 && top == 0 && bottom == 1 )
	{
		glClearDepth( 1.0 );
		glClearColor( back.r/256.0, back.g/256.0, back.b/256.0, 0 );
		glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
	}
	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
//	glFrustum( -0.1, 0.1, -0.1, 0.1, 0.1, 64 );
	const float aspect1 = dx*1.0/dy;
	const float aspect2 = dy*1.0/dx;
	glFrustum( -0.5, 0.5, -0.5*aspect2, 0.5*aspect2, 1, z_buf_range );
//	glFrustum( -1, 1, -1, 1, 1, z_buf_range );
	float pr_values[16];
	glGetFloatv( GL_PROJECTION_MATRIX, pr_values );
//	glScalef( 0.02f, 0.02f, 0.02f );
	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
	light_count = 0;
	view_mat = Matrix3D::identity();
	world_mat = Matrix3D::identity();

//	glEnable( GL_LIGHTING );
	glEnable(GL_TEXTURE_2D);
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); 
//	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST );

	glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
	glEnable( GL_BLEND );

	glAlphaFunc( GL_NOTEQUAL, 0 );
	glEnable( GL_ALPHA_TEST );

	glDisable( GL_DEPTH_TEST );
	glDepthFunc( GL_LEQUAL );
	glDepthMask( true );

	glEnable( GL_FOG );
    GLfloat fogColor[4] = {0xBC/256.0, 0xD7/256.0, 0xE7/256.0, 1}; // 0xBC, 0xD7, 0xE7 // TODO
	glFogfv( GL_FOG_COLOR, fogColor );
//	glFogi( GL_FOG_MODE, GL_EXP2 );
//	glFogf( GL_FOG_DENSITY , 1/z_buf_range );

	glFogi( GL_FOG_MODE, GL_LINEAR );
	glFogf( GL_FOG_START, z_buf_range*0.6 );
	glFogf( GL_FOG_END, z_buf_range*0.8 );

//	glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);

	ambient = Color( 250, 250, 250 ); // BUG
	GLfloat lmodel_ambient[] = { ambient.r/256.0, ambient.g/256.0, ambient.b/256.0, 1.0 };
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
	glEnable(GL_CULL_FACE);
	last_texture = "HrenomTebePolbu";
	glEnableClientState( GL_NORMAL_ARRAY );
	glEnableClientState( GL_VERTEX_ARRAY );
	glEnableClientState( GL_TEXTURE_COORD_ARRAY );
}

void HardwareGL::z_buffer( bool is_on )
{
	if( is_on )
	{
		glEnable(GL_DEPTH_TEST);
		glDepthMask( true );
	}
	else
	{
		glDisable(GL_DEPTH_TEST);
		glDepthMask( false );
	}
}

bool HardwareGL::is_point_visible( const Vector & point )
{
	Vector eye = view_mat * world_mat * point;
	float pr_values[16];
	glGetFloatv( GL_PROJECTION_MATRIX, pr_values );
	Matrix3D proj( pr_values );
	Vector scr = proj * eye;

	int port[4];
	glGetIntegerv( GL_VIEWPORT, port );
	int x = port[0];
	int y = port[1];
	int wx = port[2];
	int wy = port[3];

	if( scr.z < 0 )
		return false;
	int s_x = (scr.x/scr.z + 1)*wx/2 + x;
	int s_y = (scr.y/scr.z + 1)*wy/2 + y;
	float value = 0;
	glReadPixels( s_x, s_y, 1, 1, 
		GL_DEPTH_COMPONENT, GL_FLOAT, &value );

	return value > 0.5;
}

void HardwareGL::fog( bool is_on )
{
	if( is_on )
		glEnable( GL_FOG );
	else
		glDisable( GL_FOG );
}

void HardwareGL::clear_lights()
{
	light_count = 0;
	int max_lights = 8;
	glGetIntegerv( GL_MAX_LIGHTS, &max_lights );
	for( int i = 0; i < max_lights; ++i )
		glDisable( GL_LIGHT0 + i );
}

void HardwareGL::view_matrix( const Matrix3D & mat )
{
	view_mat = mat;
	MatrixGL( view_mat * world_mat ).save();
}

void HardwareGL::world_matrix( const Matrix3D & mat )
{
	world_mat = mat;
	MatrixGL( view_mat * world_mat ).save();
}

void HardwareGL::flip( float fl )
{
//	static int counter = 0;// ++counter;
//	if( ++counter >= 100 )
//		__asm int 3;

	if( fl != 100 )
	{
		Log("Fl == %f\n", fl );
//		float val = abs(110 - counter)/10.0;
//		if( val < 0 ) val = 0;
//		if( counter > 120 ) counter = 0;
		glLoadIdentity();
		glMatrixMode( GL_PROJECTION );
		glLoadIdentity();

		glDisable(GL_DEPTH_TEST);
		glDisable(GL_LIGHTING);
		glDisable(GL_TEXTURE_2D);
		glDisable(GL_FOG);

		glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
		glColor4f( 0,0,0,1.0 - fl/100 );
//		glBlendFunc( GL_SRC_ALPHA, GL_ONE );
//		glColor4f( 1,1,1,1.0 - fl/100 );

		glBegin( GL_POLYGON );
		glVertex2f(  1, -1 );
		glVertex2f(  1,  1 );
		glVertex2f( -1,  1 );
		glVertex2f( -1, -1 );
		glEnd();
	}
	glFlush();
	tkSwapBuffers();
}

string HardwareGL::get_and_clear_error()
{
	string result;
	while( int err = glGetError() )
	{
		char buf[128];
		sprintf( buf,"%x | ", err );
		result += buf;
	}
	return result;
}

void HardwareGL::add_light( const Light3D & light )
{
	int max_lights = 8;
	glGetIntegerv( GL_MAX_LIGHTS, &max_lights );
	if( light_count >= max_lights )
		return ;
	int li = GL_LIGHT0 + light_count++;

	glPushMatrix();

	MatrixGL( view_mat ).save();
	GLfloat light1_ambient[] = { 0.1f, 0.1f, 0.1f, 1.0 };
	const Color * c = &light.diffuse;
	GLfloat light1_diffuse[] = { c->r/256.0, c->g/256.0, c->b/256.0, 1.0 };
	c = &light.specular;
	GLfloat light1_specular[] = { c->r/256.0, c->g/256.0, c->b/256.0, 1.0 };
	const Vector * v = &light.position;
	GLfloat light1_position[] = { v->x, v->y, v->z, 1.0 };

	glLightfv( li, GL_AMBIENT, light1_ambient );
	glLightfv( li, GL_DIFFUSE, light1_diffuse );
	glLightfv( li, GL_SPECULAR, light1_specular );
	glLightfv( li, GL_POSITION, light1_position );
	glLightf(  li, GL_CONSTANT_ATTENUATION, 1 );
	glLightf(  li, GL_LINEAR_ATTENUATION, 1 );
	glLightf(  li, GL_QUADRATIC_ATTENUATION, 0 );
	glEnable( li );

	glPopMatrix();
}

void HardwareGL::set_material( const Shader3D & shader )
{
	const Color * c = &shader.ambient;
//	float alpha = std::max(shader.diffuse.a, shader.ambient.a)/256.0;
	float alpha = shader.diffuse.a/255.0;
	if( alpha == 0 )
		alpha = 255.0;
//	GLfloat mat_ambient[] = { c->r/256.0, c->g/256.0, c->b/256.0, 1.0 };
	GLfloat mat_ambient[] = { 1.0, 1.0, 1.0, 1.0 };
	c = &shader.diffuse;
	GLfloat mat_diffuse[] = { c->r/256.0, c->g/256.0, c->b/256.0, alpha };

	glColor4fv( mat_diffuse );

	c = &shader.specular;
	GLfloat mat_specular[] = { c->r/256.0, c->g/256.0, c->b/256.0, 1.0 };
	c = &shader.emission;
	GLfloat mat_emission[] = { c->r/256.0, c->g/256.0, c->b/256.0, 1.0 };
	GLfloat high_shininess[] = { 100.0 };

    glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
    glMaterialfv(GL_FRONT, GL_SHININESS, high_shininess);
    glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);

	static int table[] = { GL_ZERO, GL_ONE,
		GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR,
		GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
		GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA,
		GL_DST_COLOR, GL_ONE_MINUS_DST_COLOR,
		GL_SRC_ALPHA_SATURATE };


	glBlendFunc( table[shader.source_blend], table[shader.dest_blend] );

	if (last_texture == shader.texture_name)
		return;
	last_texture = shader.texture_name;

	if( shader.generated_data )
	{
		bool need_validate = shader.need_validate;
		std::map<string, int>::iterator it = textures.find( shader.texture_name );
		if( it == textures.end() )
		{
			GLuint tex_id;
			glGenTextures( 1, &tex_id );
			glBindTexture( GL_TEXTURE_2D, tex_id );
			it = textures.insert( std::make_pair( shader.texture_name, tex_id ) ).first;
			need_validate = true; // first time validate always
		}
		else
		{
			glBindTexture( GL_TEXTURE_2D, it->second );
		}

		if( need_validate )
		{
			int error=gluBuild2DMipmaps(GL_TEXTURE_2D, 1,
				shader.generated_cx, shader.generated_cy,
				GL_LUMINANCE, GL_UNSIGNED_BYTE, shader.generated_data );
		}
		return;
	}

	if( (shader.texture_name=="") )
	{
		last_texture == "";
		glBindTexture( GL_TEXTURE_2D, 0 );
		return;
	}

//	bool need_validate = shader.need_validate;

	std::map<string, int>::iterator it = textures.find( shader.texture_name );
	if( it == textures.end() )
	{
		if( std::find( failed_textures.begin(), failed_textures.end(), shader.texture_name ) != failed_textures.end() )
		{
			glBindTexture( GL_TEXTURE_2D, 0 );
			return;
		}

		FILE * file = fopen( shader.texture_name.c_str(), "rb" );
		if( NULL == file )
		{
			glBindTexture( GL_TEXTURE_2D, 0 );
			failed_textures.push_back( shader.texture_name );
			return;
		}
//		need_validate = true;
		struct TargaHeader
		{
			unsigned char IDLength;
			unsigned char ColormapType;
			unsigned char ImageType;
			unsigned char ColormapSpecification[5];
			unsigned short XOrigin;
			unsigned short YOrigin;
			unsigned short ImageWidth;
			unsigned short ImageHeight;
			unsigned char PixelDepth;
			unsigned char ImageDescriptor;
		} tga;

		fread( &tga, sizeof(TargaHeader), 1, file );

		GLuint tex_id;
//		glEnable(GL_TEXTURE_2D);
		glGenTextures( 1, &tex_id );
		glBindTexture( GL_TEXTURE_2D, tex_id );

		// Only true color, non-mapped images are supported
		if( ( 0 != tga.ColormapType ) || 
			( tga.ImageType != 10 && tga.ImageType != 2 ) )
		{
			glBindTexture( GL_TEXTURE_2D, 0 );
			failed_textures.push_back( shader.texture_name );

			fclose( file );
			return;
		}
	    // Skip the ID field. The first byte of the header is the length of this field
		if( tga.IDLength )
			fseek( file, tga.IDLength, SEEK_CUR );

		unsigned char * data = snew unsigned char[4*tga.ImageWidth*tga.ImageHeight];
		for( DWORD y=0; y<tga.ImageHeight; y++ )
		{
			unsigned char * curr = data + 4* y * tga.ImageWidth;
			for( DWORD x=0; x<tga.ImageWidth; x++ )
			{
	            unsigned char b = getc( file );
                unsigned char g = getc( file );
                unsigned char r = getc( file );
                unsigned char a = 0xff;
				if( tga.PixelDepth == 32 )
				{
					a = getc( file );
					if( !b && !r && !g )
						a = 0;
				}
				*curr++ =  r;
				*curr++ =  g;
				*curr++ =  b;
				*curr++ =  a;
			}
		}
		fclose( file );
		int error=gluBuild2DMipmaps(GL_TEXTURE_2D, 4,
			tga.ImageWidth, tga.ImageHeight,
			GL_RGBA, GL_UNSIGNED_BYTE, data );
		delete [] data; data = NULL;
		it = textures.insert( std::make_pair( shader.texture_name, tex_id ) ).first;
	}
	else
		glBindTexture( GL_TEXTURE_2D, it->second );
/*	if( shader.generated_data )
	{
		int error=gluBuild2DMipmaps(GL_TEXTURE_2D, 1, 
			shader.generated_cx, shader.generated_cy,
			GL_LUMINANCE, GL_UNSIGNED_BYTE, shader.generated_data );
	}*/
}

void HardwareGL::draw_triangle_list( Vertex3D * vertex, unsigned count )
{
	if( 0 )
	{
		glBegin( GL_TRIANGLES );
		for( int i = 0; i < count; ++i )
		{
			glTexCoord2f( vertex[ i ].u, vertex[ i ].v );
			glNormal3f( vertex[ i ].normal.x, vertex[ i ].normal.y, vertex[ i ].normal.z );
			glVertex3f( vertex[ i ].pos.x, vertex[ i ].pos.y, vertex[ i ].pos.z );
		}
		glEnd();
	}
	else
	{
		unsigned stride = (unsigned char *)( &vertex[1].u ) - (unsigned char *)( &vertex[0].u );
		glNormalPointer( GL_FLOAT, stride, &vertex[0].normal.x );
		glVertexPointer( 3, GL_FLOAT, stride, &vertex[0].pos.x );
		glTexCoordPointer( 2, GL_FLOAT, stride, &vertex[0].u );
 
		glDrawArrays( GL_TRIANGLES, 0, count );
	}
}

void HardwareGL::draw_indexed_triangle_list( Vertex3D * vertex, unsigned count, 
	unsigned short * indexes, unsigned ind_count )
{
	unsigned stride = (unsigned char *)( &vertex[1].u ) - (unsigned char *)( &vertex[0].u );
	glNormalPointer( GL_FLOAT, stride, &vertex[0].normal.x );
	glVertexPointer( 3, GL_FLOAT, stride, &vertex[0].pos.x );
	glTexCoordPointer( 2, GL_FLOAT, stride, &vertex[0].u );
	glBegin( GL_TRIANGLES );
	for( int i = 0; i < ind_count; ++i )
	{
		glArrayElement( indexes[i] );
	}
	glEnd();
}